#ifndef FRINK_PARSER_H
#define FRINK_PARSER_H

#include <vector>
#include <string>
#include <iostream>
#include <memory>
#include <map>

namespace Parser {

enum class TokenType {
    open = 1,  // for debugging, token=0 is invalid
    identifier,
    keyword,
    op,
    number,
    variable,
    close,
    eol,
    comment,
    assignment
};

struct Token {
    std::string str;
    TokenType type;
};

typedef std::vector<Parser::Token>::const_iterator TokenConstIt;

std::vector<Token> lexStream(std::istream &istr);
int tokenColor(TokenType type);
void printTokens(std::vector<Token> const &tokens, std::ostream &ostr);
void printTokens(TokenConstIt first, TokenConstIt last, std::ostream &ostr);


// check if string is variable, i.e. begins with a '?'
inline bool isVariable(std::string const &str) {
    return str[0] == '?';
}

typedef std::map<std::string, std::string> VariableReplacements;

class GdlTerm {
  public:
    virtual std::string toString() const = 0;
    virtual void addVariablesToMap(std::map<std::string, std::vector<GdlTerm const *>> &map) const = 0;

    virtual ~GdlTerm() {}
};

class GdlProposition : public GdlTerm {
  public:
    std::vector<std::string> strs;

    GdlProposition() : strs{}
    {
    }
    GdlProposition(std::string s) : strs{s}
    {
    }
    GdlProposition(TokenConstIt first, TokenConstIt last);

    virtual std::string toString() const override {
        std::string ret;
        for (auto const &s : strs) {
            ret += s;
            ret += " ";
        }
        ret.pop_back(); // remove last space
        return ret;
    }

    bool hasVariable() const {
        for (auto const &s : strs) {
            if (isVariable(s))
                return true;
        }
        return false;
    }

    virtual void addVariablesToMap(std::map<std::string, std::vector<GdlTerm const *>> &map) const override
    {
        for (auto const &s : strs) {
            if (isVariable(s))
                map[s].push_back(this);
        }
    }

    void addVariablesToMap(std::map<std::string, std::vector<GdlTerm const *>> &map, GdlTerm const *gdlterm) const
    {
        for (auto const &s : strs) {
            if (isVariable(s))
                map[s].push_back(gdlterm);
        }
    }

    std::string toString(VariableReplacements const &variable_replacement_map) const {
        std::string ret;
        for (auto const &s : strs) {
            auto replacement = variable_replacement_map.find(s);
            if (replacement != variable_replacement_map.end()) {
                ret += replacement->second;
            } else {
                ret += s;
            }
            ret += " ";
        }
        ret.pop_back(); // remove last space
        return ret;
    }
};

enum class GdlKeyword {
    role,
    input,
    base,
    init,
    kw_true,
    does,
    next,
    legal,
    goal,
    terminal
};
class GdlKeywordProposition : public GdlTerm {
  public:
    std::string keyword;
    std::string extra; // extra string, for example 'xplayer' in "does xplayer (mark 1 1)"
    GdlProposition term; // the proposition after the keyword

    virtual std::string toString() const override
    {
        return toString(VariableReplacements{});
    }
    std::string toString(VariableReplacements const &variable_replacement_map) const
    {
        return keyword + " " + extraToString(variable_replacement_map) + " (" +
               term.toString(variable_replacement_map) + ")";
    }

    std::string extraToString(VariableReplacements const &variable_replacement_map) const {
        auto replacement = variable_replacement_map.find(extra);
        if (replacement != variable_replacement_map.end()) {
            return replacement->second;
        }
        return extra;
    }

    virtual void addVariablesToMap(std::map<std::string, std::vector<GdlTerm const*>> &map) const override {
        if (isVariable(extra)) {
            map[extra].push_back(this);
        }
        term.addVariablesToMap(map, this);
    }
};

class GdlNot : public GdlTerm {
  public:
    std::unique_ptr<GdlTerm> term;

    std::string toString() const { return std::string("not (") + term->toString() + ")"; }

    virtual void addVariablesToMap(std::map<std::string, std::vector<GdlTerm const*>> &map) const override
    {
        term->addVariablesToMap(map);
    }
};
class GdlOr : public GdlTerm {
  public:
    std::vector<std::unique_ptr<GdlTerm> > terms;

    std::string toString() const
    {
        std::string ret = "or ";
        for (auto &s : terms)
            ret += " (" + s->toString() + ")";
        return ret;
    }

    virtual void addVariablesToMap(std::map<std::string, std::vector<GdlTerm const*>> &map) const override
    {
        for (auto const &t : terms) {
            t->addVariablesToMap(map);
        }
    }   
};
class GdlAnd : public GdlTerm {
  public:
    std::vector<std::unique_ptr<GdlTerm> > terms;

    std::string toString() const
    {
        std::string ret = "and ";
        for (auto &s : terms)
            ret += " (" + s->toString() + ")";
        return ret;
    }

    virtual void addVariablesToMap(std::map<std::string, std::vector<GdlTerm const*>> &map) const override
    {
        for (auto const &t : terms) {
            t->addVariablesToMap(map);
        }
    }   
};

class GdlDistinct : public GdlTerm {
  public:
    std::string first;
    std::string second;
    std::string toString() const { return std::string("distinct ") + first + " " + second; }

    virtual void addVariablesToMap(std::map<std::string, std::vector<GdlTerm const*>> &map) const override
    {
        if (isVariable(first))
            map[first].push_back( this );
        if (isVariable(second))
            map[second].push_back( this );
    }   
};

class GdlAssignment : public GdlTerm { // hmm is not really a term, can only be used as first term of sentence
  public:
    std::vector<std::unique_ptr<GdlTerm> > terms;

    std::string toString() const
    {
        std::string ret = "<= ";
        for (auto &s : terms)
            ret += " (" + s->toString() + ")";
        return ret;
    }

    virtual void addVariablesToMap(std::map<std::string, std::vector<GdlTerm const*>> &map) const override
    {
        // don't add first to the map, because we want to assign possibly to a yet non-existing propostion
        for (auto t = terms.begin() +1, last = terms.end(); t != last; ++t) {
            (*t)->addVariablesToMap(map);
        }
    }   
};

class GdlSentence {
  public:
    std::unique_ptr<GdlTerm> term;

    std::string toString() const {
        return std::string("(") + term->toString() + ")";
    };

    std::map<std::string, std::vector<GdlTerm const*>> getVariables() const {
        std::map<std::string, std::vector<GdlTerm const*>> ret;
        term->addVariablesToMap(ret);
        return ret;
    }
};

std::vector<GdlSentence> parseTokens(std::vector<Parser::Token>::const_iterator first,
                                     std::vector<Parser::Token>::const_iterator last);

inline std::vector<GdlSentence> parseTokens(std::vector<Parser::Token> tokens)
{
    return parseTokens(tokens.begin(), tokens.end());
}


} // namespace Parser

#endif // FRINK_PARSER_H